package alibaba.server.service;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import alibaba.common.CheckEntry;
import alibaba.common.utils.Base64;
import alibaba.common.utils.CommonUtils;
import alibaba.common.utils.SignatureUtils;
import alibaba.server.Listing;
import alibaba.server.ListingEntry;
import alibaba.server.Snapshots;

public class BackupServiceImpl implements BackupService {

	private static final String META_DATA_FILE_NAME = "listing.dat";
	private HashMap<String,Integer> referenceCounterMap;
	private String backupDirectory;
	private String metaDateFilePath;
	private Listing listing;
	private Map<String,Snapshots> userSnapshots;
	
	@Override
	public boolean init(String backupDirectory) throws IOException {
		this.backupDirectory = CommonUtils.makeEndWithUnixSeparator(backupDirectory);
		metaDateFilePath = this.backupDirectory + META_DATA_FILE_NAME;
		
		// Check if given directory exist, if not create it:
		File directory = new File(this.backupDirectory);		
		if (!directory.isDirectory()) {
			directory.mkdirs();
		}
	
		// Load listing file, create a new one if necessary:
		File metaDataFile = new File(metaDateFilePath);
		if (!metaDataFile.exists()) {
			listing = new Listing();
			Listing.writeListing(metaDateFilePath, listing);
		} else {
			listing = Listing.readListing(metaDateFilePath);
		}
		
		// Create referenceCounterMap:
		referenceCounterMap = new HashMap<String, Integer>();
	    for (File file : new File(backupDirectory).listFiles()) {
	    	if (file.equals(metaDataFile)) {
	    		continue;
	    	}
	    	if (!file.isFile()) {
	    	    return false;
	    	}
	    	byte[] signature = SignatureUtils.sign(file.getAbsolutePath());
	    	String signatureBase64 = Base64.encodeBytes(signature, Base64.URL_SAFE);
	    	referenceCounterMap.put(signatureBase64, 0);
		}
	    
	    // Create userSnapshots:
	    userSnapshots = new HashMap<String, Snapshots>();
	    for (ListingEntry entry : listing.getListOfFiles().values()) { 
	        
	        // we have to check this here
	        if (entry.getSnapshotVersion() == null) {
	            return false;
	        }
	        
	    	Snapshots snapshots;
	    	if (!userSnapshots.containsKey(entry.getOwner())) {
	    		snapshots = new Snapshots(entry.getOwner());
	    		userSnapshots.put(entry.getOwner(), snapshots);
	    	} else {
	    		snapshots = userSnapshots.get(entry.getOwner());
	    	}
	    	
	    	for (int snapshotVersion : entry.getSnapshotVersion()) {
	    		snapshots.addSnapshotEntry(snapshotVersion, entry);
			}
	    }
	    
	    // Check consistency:
	    for (ListingEntry entry : listing.getListOfFiles().values()) {    
	    	// Check if in a snapshot:
	    	if (entry.getSnapshotVersion().length == 0) {
	    		return false;
	    	}
	    	
	    	// Check if no double snapshots:
	    	int counter = 0;
	    	for (int ver1 : entry.getSnapshotVersion()) {
	    		counter = 0;
		    	for (int ver2 : entry.getSnapshotVersion()) {
		    		counter += (ver1 == ver2) ? 1 : 0;
		    		if (counter > 1) {
		    			return false;
		    		}
				}
			}
	    	
	    	// Check if valid file:
	        File file = new File(entry.getLocalName());
	        if (!file.isFile()) {
	        	return false;
	        } 
	        
	        // Check if exist in listing file:
	        byte[] currentSignature = SignatureUtils.sign(file.getAbsolutePath());  
	        String currentSignatureBase64 = Base64.encodeBytes(currentSignature, Base64.URL_SAFE);
	        if (!referenceCounterMap.containsKey(currentSignatureBase64)) {
	            return false;
	        }
	        
	        // Check if valid local name:
	        if (!entry.getLocalName().equals(createLocalFilePath(currentSignatureBase64))) {
	        	return false;
	        }
	        
	        // Check signature: 
            if (!Arrays.equals(currentSignature, entry.getFileHash())) {
                return false;
            }  
                       
            // Update reference counter:
            increaseReferenceCounter(currentSignatureBase64);
	    }
	    
	    // Check that there ain't any extra files in backupDirectory:
	    for (Integer referenceCounter : referenceCounterMap.values()) {
			if (referenceCounter == 0) {
				return false;
			}
		}
	    return true;
	}

	private String createLocalFilePath(String signatureBase64) throws IOException {
	    StringBuilder sb = new StringBuilder();
	    sb.append(backupDirectory);
	    sb.append(signatureBase64);
	    return sb.toString();
	}
	
	private String createFilePath(String user, String filePath, int snapshotNumber) {
	    return user + "@" + snapshotNumber + "@" + filePath;
	}
	
	private void increaseReferenceCounter(String signatureBase64) {
		if (!referenceCounterMap.containsKey(signatureBase64)) {
			referenceCounterMap.put(signatureBase64, 1);
		} else {
			int oldValue = referenceCounterMap.get(signatureBase64);
			referenceCounterMap.put(signatureBase64, oldValue+1);
		}
	}
	
	private void decreaseReferenceCounter(byte[] signature) throws IOException  {
		decreaseReferenceCounter(Base64.encodeBytes(signature, Base64.URL_SAFE));
	}
	
	private void decreaseReferenceCounter(String signatureBase64) throws IOException {
    	int oldValue = referenceCounterMap.get(signatureBase64);
    	if (oldValue == 1) {
    		referenceCounterMap.remove(signatureBase64);
    		File file = new File(createLocalFilePath(signatureBase64));
    		file.delete();
    	} else {
    		referenceCounterMap.put(signatureBase64, oldValue-1);
    	}
	}
	
	@Override
	public synchronized boolean backupFileSignature(String user, String filePath, String signatureBase64) throws IOException {
		if (referenceCounterMap.containsKey(signatureBase64)) {
			byte[] signature = Base64.decode(signatureBase64, Base64.URL_SAFE);
			
			// Update old entry if exist:
			String userFilePath = createFilePath(user, filePath, 0);
			ListingEntry oldEntry = listing.getListOfFiles().get(userFilePath);
			ListingEntry newEntry = null;
			
			if (oldEntry != null) {
			    if (!Arrays.equals(signature, oldEntry.getFileHash())) {
			    	// Old file is modified
    				removeCurrentSnapshot(oldEntry);
    				userSnapshots.get(user).getSnapshotEntries(0).remove(oldEntry);
			    } else {	
			        // Same file in current snapshot already exists in user entries.. no need to continue
			        return true;
			    }
			} 
			
			// check if file already exists in previous snapshots
			if ((newEntry = findFileEntryInUserSnapshots(user, filePath, signature)) != null) {
			    addCurrentSnapshotToExistingEntry(newEntry);  
			} else {
			    // Create new listing entry:
                String localFilePath = createLocalFilePath(signatureBase64);
                newEntry = new ListingEntry(filePath, localFilePath, signature);
                newEntry.setOwner(user);
                newEntry.setSnapshotVersion(new int[] { 0 });
                increaseReferenceCounter(signatureBase64);	    
			}
			
			// update userSnapshots and listing file:
			listing.getListOfFiles().put(userFilePath, newEntry);
			addNewEntryToUserSnapshots(user,newEntry);
			Listing.writeListing(metaDateFilePath, listing);				
			return true;			
		} 
		return false;
	}
	
	@Override
	public synchronized boolean backupFileData(String user, String filePath, File file) throws IOException {
	    
	    byte[] newFileSignature = SignatureUtils.sign(file.getAbsolutePath());
	    
		// Update old entry if exist:
		String userFilePath = createFilePath(user, filePath, 0); 
		ListingEntry oldEntry = listing.getListOfFiles().get(userFilePath);
		ListingEntry newEntry = null;
		
		if (oldEntry != null) {
		    if (!Arrays.equals(oldEntry.getFileHash(), newFileSignature)) {
                // Old file is modified
                removeCurrentSnapshot(oldEntry);
                userSnapshots.get(user).getSnapshotEntries(0).remove(oldEntry);
		    } else {
		        // Same file in current snapshot already exists in user entries.. no need to continue
		        return true;
		    }
		}

		String signatureBase64 = Base64.encodeBytes(newFileSignature, Base64.URL_SAFE);

        // check if file already exists in previous snapshots
        if ((newEntry = findFileEntryInUserSnapshots(user, filePath, newFileSignature)) != null) {
            // nothing to do with recieved file
            boolean success = file.delete();
            if (!success) {
                return false;
            }
            addCurrentSnapshotToExistingEntry(newEntry);  
        } else {
            // Create new listing entry:
            String localFilePath = createLocalFilePath(signatureBase64);
            newEntry = new ListingEntry(filePath, localFilePath, newFileSignature);
            newEntry.setOwner(user);
            newEntry.setSnapshotVersion(new int[] { 0 });
            increaseReferenceCounter(signatureBase64);   
            
            // Save file:
            boolean success;
            File localFile = new File(localFilePath);
            if (localFile.isFile()) {
                // file already exists... nothing to do with recieved file
                success = file.delete();
            } else {
                success = file.renameTo(localFile);
            }
            if (!success) {
                return false;
            }
        }
		
        addNewEntryToUserSnapshots(user, newEntry);
        listing.getListOfFiles().put(userFilePath, newEntry);
        Listing.writeListing(metaDateFilePath, listing);
		return true;
	}
	
	private void addNewEntryToUserSnapshots(String user, ListingEntry newEntry) {
        if (!userSnapshots.containsKey(user)) { 
            userSnapshots.put(user, new Snapshots(user));
        }
        userSnapshots.get(user).addSnapshotEntry(0, newEntry);
	}

	private void removeCurrentSnapshot(ListingEntry oldEntry) throws IOException {
		// check if entry belongs to other snapshots
		if (oldEntry.getSnapshotVersion().length > 1) {
		    // remove current snapshot from entry (lies in the first position)
		    oldEntry.setSnapshotVersion(Arrays.copyOfRange(
		            oldEntry.getSnapshotVersion(), 1, 
		            oldEntry.getSnapshotVersion().length));
		} else {
			decreaseReferenceCounter(oldEntry.getFileHash());
		}
	}

	 /** 
     * @return a listing entry containing the file or null if none found
     */
    private ListingEntry findFileEntryInUserSnapshots(String user, String filePath, byte[] signature) {
        if (userSnapshots.containsKey(user)) {
            for (Integer snapshotNumber : userSnapshots.get(user).getAvailableSnapshots()) {            
                String path = createFilePath(user,filePath,snapshotNumber);
                ListingEntry entry = listing.getListOfFiles().get(path);
                if (entry != null && Arrays.equals(signature, entry.getFileHash())) {
                    return entry;
                }
            }   
        }
        return null;
    }
    
    private void addCurrentSnapshotToExistingEntry(ListingEntry entry) {
        // Add current snapshot (snapshot 0) to snapshotsVersionsArray
        int[] snapshotsVersions = new int[entry.getSnapshotVersion().length+1];
        snapshotsVersions[0] = 0;
        for (int i = 0; i < entry.getSnapshotVersion().length; i++) {
            snapshotsVersions[i+1] = entry.getSnapshotVersion()[i];     
        }
        entry.setSnapshotVersion(snapshotsVersions);
    }
	
	@Override
	public byte[] getSignature(String user, String filePath, int snapshotNumber) {
	    ListingEntry entry = listing.getListOfFiles().get(createFilePath(user,filePath,0));
	    return (entry == null) || !isEntrySnapshotVersion(entry,snapshotNumber) ? null : entry.getFileHash();
	}

	@Override
	public List<String> listDirectory(String user, String dirPath, int snapshotNumber) {
		List<String> result = new ArrayList<String>();
		
		// No entries in given snapshot:
		if (userSnapshots.get(user) == null || !userSnapshots.get(user).containsSnapshot(snapshotNumber)) {
			return result;
		}
		
		// Run on snapshot entries:
		for (ListingEntry listingEntry : userSnapshots.get(user).getSnapshotEntries(snapshotNumber)) {
			String backupFile = listingEntry.getFilename();
			if (backupFile.startsWith(dirPath)) {
				String suffix = backupFile.substring(dirPath.length());
				if (suffix.indexOf(CommonUtils.UNIX_SEPARATOR) == -1) {
					result.add(listingEntry.getFilename());
				}
			}
		}
		return result;
	}
	
	private boolean isEntrySnapshotVersion(ListingEntry entry, int snapshotNumber) {
		for (int snapshot : entry.getSnapshotVersion()) {
			if (snapshot == snapshotNumber) {
				return true;
			}
		}
		return false;
	}

	@Override
	public synchronized File restoreFile(String user, String filePath, int snapshotNumber) {
		ListingEntry listingEntry = listing.getListOfFiles().get(createFilePath(user,filePath,snapshotNumber)); 
		if (listingEntry != null) {
			File file = new File(listingEntry.getLocalName());
			if (file.isFile()) {
				return file;
			}
		}
		return null;
	}

    private List<ListingEntry> listDirectoryEntries(String user, String path) {
        
        List<ListingEntry> result = new ArrayList<ListingEntry>();
        
        // we do this because there are multiple references to the same entry
        Set<ListingEntry> uniqueEntries = new HashSet<ListingEntry>(listing.getListOfFiles().values());
        
        for (ListingEntry listingEntry : uniqueEntries) {
            String backupFile = listingEntry.getFilename();
            if (listingEntry.getOwner().equals(user) && backupFile.startsWith(path)) {
                String suffix = backupFile.substring(path.length());
                if (suffix.indexOf(CommonUtils.UNIX_SEPARATOR) == -1) {
                    result.add(listingEntry);
                }
            }
        }
        return result;
        
    }
	
	@Override
	public List<CheckEntry> check(String user, String path) throws IOException {
	    
	    boolean stateChanged = false;
	    
		List<ListingEntry> listDirectory = listDirectoryEntries(user, path);
		
		List<CheckEntry> result = new ArrayList<CheckEntry>(listDirectory.size());
		
		for (ListingEntry entry : listDirectory) {
            byte[] signature;
            if (new File(entry.getLocalName()).isFile()) {
                signature = SignatureUtils.sign(entry.getLocalName());
                if (!Arrays.equals(signature, entry.getFileHash())) {
                    entry.setFileHash(SignatureUtils.EMPTY_SIGNATURE);
                    stateChanged = true;
                }
            } else {
                entry.setFileHash(SignatureUtils.EMPTY_SIGNATURE);
                stateChanged = true;
            }
            
            byte[] hashSigCopy = Arrays.copyOf(entry.getFileHash(), entry.getFileHash().length);
            int[] snapshotVersionsCopy = Arrays.copyOf(entry.getSnapshotVersion(), entry.getSnapshotVersion().length);
            
            CheckEntry checkEntry = new CheckEntry(entry.getFilename(), snapshotVersionsCopy, hashSigCopy);
            
            result.add(checkEntry);
		}
		
		if (stateChanged) {
            Listing.writeListing(metaDateFilePath, listing);
		}
		
		return result;
	}

    @Override
    public boolean createSnapshot(String user, int snapshotNumber) {
    	if (snapshotNumber == 0) {
    		return false;
    	}
    	
        if (!userSnapshots.containsKey(user)) {
            userSnapshots.put(user, new Snapshots(user));
        }
        
        if (userSnapshots.get(user).containsSnapshot(snapshotNumber)) {
            return false;
        }
        
        if (!userSnapshots.get(user).containsSnapshot(0)) {
            // TODO DAN
            // not quite sure what to do.. 
            return true;
        }
        
        Set<ListingEntry> currentSnapshot = userSnapshots.get(user).getCurrentSnapshot();
        
        for (ListingEntry entry : currentSnapshot) {
            listing.getListOfFiles().put(createFilePath(user, entry.getFilename(), snapshotNumber), entry);
            userSnapshots.get(user).addSnapshotEntry(snapshotNumber, entry);
            int[] newSnapshotVersions = Arrays.copyOf(entry.getSnapshotVersion(), entry.getSnapshotVersion().length+1);
            newSnapshotVersions[newSnapshotVersions.length-1] = snapshotNumber;
            entry.setSnapshotVersion(newSnapshotVersions);
        }
        
        try {
            Listing.writeListing(metaDateFilePath, listing);
        } catch (FileNotFoundException e) {
            throw new IllegalStateException("Critical error: failed finding listing file");
        }
        
        return true;
    }

    @Override
    public boolean deleteSnapshot(String user, int snapshotNumber) throws IOException {
    	if (snapshotNumber == 0) {
    		return false;
    	}
    	
        if (userSnapshots.get(user) == null || !userSnapshots.get(user).containsSnapshot(snapshotNumber)) {
            return false;
        }
        
        for (ListingEntry entry : userSnapshots.get(user).getSnapshotEntries(snapshotNumber)) {
        	if (entry.getSnapshotVersion().length > 1) {
        		// Remove snapshot number from array:
		    	int[] snapshotsVersions = new int[entry.getSnapshotVersion().length-1];
	        	int j = 0;
	        	for (int i = 0; i < entry.getSnapshotVersion().length; i++) {
	        		if (entry.getSnapshotVersion()[i] == snapshotNumber) {
	        			continue;
	        		}
	        		snapshotsVersions[j] = entry.getSnapshotVersion()[i];	
	        		j++;
				}
	        	entry.setSnapshotVersion(snapshotsVersions);
        	} else {
        	    // no snapshot is pointing to this entry so we decrease the reference count
        	    decreaseReferenceCounter(entry.getFileHash());
        	}
        	// this should always happen, as each snapshot that belongs to an entry points to it in the listing
        	listing.getListOfFiles().remove(createFilePath(user, entry.getFilename(), snapshotNumber));
        }
        
        userSnapshots.get(user).removeSnapshot(snapshotNumber);
        Listing.writeListing(metaDateFilePath, listing); 
        return true;
    }

}
